package com.bby.disk.manager.download;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.SerializeUtil;
import com.bby.common.config.GlobalSettings;
import com.bby.common.exception.BBYException;
import com.bby.common.utils.BBYUtils;
import com.bby.common.utils.StatusCode;
import com.bby.disk.bean.req.ThumbnailReq;
import com.bby.disk.exception.BBYExceptionHandler;
import com.bby.disk.manager.tempfile.TempFileManager;
import com.bby.disk.manager.virtual.FileType;
import com.bby.disk.manager.virtual.VirtualFile;
import com.bby.disk.util.BBYFileSystem;
import com.bby.disk.util.VideoImgUtil;
import net.coobird.thumbnailator.Thumbnails;
import org.bytedeco.javacv.FrameGrabber;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.params.SetParams;

import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.attribute.FileTime;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Component
public class DownloadManager {
    JedisPool jedisPool;
    Logger logger = LoggerFactory.getLogger(this.getClass());

    public DownloadManager(JedisPool jedisPool) {
        this.jedisPool = jedisPool;
    }


    /**
     * 创建文件的直链
     *
     * @param file 文件
     */
    public DirectLinkObj createDirectLink(VirtualFile file) throws BBYException {
        DirectLinkObj directLink = new DirectLinkObj(file);
        logger.debug("添加直链：{}", directLink);
        try (Jedis jedis = jedisPool.getResource()) {
            if (jedis.exists(file.getId())) {
                jedis.expire(file.getId(), GlobalSettings.TEMP_FILE_MILLISECOND);
                return SerializeUtil.deserialize(
                        jedis.get(file.getId().getBytes(StandardCharsets.UTF_8)));
            }
            jedis.set(file.getId().getBytes(StandardCharsets.UTF_8), SerializeUtil.serialize(directLink),
                    new SetParams().ex(GlobalSettings.TEMP_FILE_MILLISECOND));
            return directLink;
        } catch (Exception e) {
            throw BBYExceptionHandler.REDIS_ERROR_EX;
        }
    }

    public Map<String, String> createDirectLinks(List<VirtualFile> list) {
        Map<String, String> map = new HashMap<>();
        try (Jedis jedis = jedisPool.getResource()) {
            for (VirtualFile file : list) {
                DirectLinkObj directLink;
                if (jedis.exists(file.getId())) {
                    directLink = SerializeUtil.deserialize(
                            jedis.get(file.getId().getBytes(StandardCharsets.UTF_8)));
                    jedis.expire(file.getId(), GlobalSettings.TEMP_FILE_MILLISECOND);
                } else {
                    directLink = new DirectLinkObj(file);
                    jedis.set(file.getId().getBytes(StandardCharsets.UTF_8), SerializeUtil.serialize(directLink),
                            new SetParams().ex(GlobalSettings.TEMP_FILE_MILLISECOND));
                }
                map.put(file.getId(), directLink.getId());
            }
        }
        return map;
    }

    public DirectLinkObj getDirectLink(String fileId, String directId) throws BBYException {
        try (Jedis jedis = jedisPool.getResource()) {
            if (!jedis.exists(fileId)) {
                throw BBYExceptionHandler.FILE_NOT_FOUNT;
            }
            DirectLinkObj directLink = SerializeUtil.deserialize(
                    jedis.get(fileId.getBytes(StandardCharsets.UTF_8)));
            if (!directLink.getId().equals(directId)) {
                throw BBYExceptionHandler.FILE_NOT_FOUNT;
            }
            logger.debug("获取直链：{}", directLink);
            return directLink;
        } catch (Exception e) {
            throw BBYExceptionHandler.REDIS_ERROR_EX;
        }
    }

    public DirectLinkObj createTempDirectLink(String fileId, String filename, String tempFilePath) throws BBYException {
        DirectLinkObj directLink = new DirectLinkObj(fileId,filename,tempFilePath);
        logger.debug("添加直链：{}", directLink);
        try (Jedis jedis = jedisPool.getResource()) {
            jedis.set(("T_"+fileId).getBytes(StandardCharsets.UTF_8), SerializeUtil.serialize(directLink),
                    new SetParams().ex(GlobalSettings.TEMP_FILE_MILLISECOND));
            return directLink;
        } catch (Exception e) {
            throw BBYExceptionHandler.REDIS_ERROR_EX;
        }
    }
    public DirectLinkObj getTempFileDirectLink(String fileId, String directId) throws BBYException {
        try (Jedis jedis = jedisPool.getResource()) {
            String id = "T_"+fileId;
            if (!jedis.exists(id)) {
                throw BBYExceptionHandler.FILE_NOT_FOUNT;
            }
            DirectLinkObj directLink = SerializeUtil.deserialize(
                    jedis.get(id.getBytes(StandardCharsets.UTF_8)));
            if (!directLink.getId().equals(directId)) {
                throw BBYExceptionHandler.FILE_NOT_FOUNT;
            }
            logger.debug("获取直链：{}", directLink);
            return directLink;
        } catch (Exception e) {
            throw BBYExceptionHandler.REDIS_ERROR_EX;
        }
    }

    public void deleteDirectLink(DirectLinkObj directLinkObj) {
        try (Jedis jedis = jedisPool.getResource()) {
            jedis.del(directLinkObj.fileId);
            if (directLinkObj.tempFilePath!=null){
                new File(directLinkObj.getTempFilePath()).delete();
            }
            logger.debug("删除直链：{}", directLinkObj);
        } catch (Exception e) {}
    }

    /**
     * 即可看图也可看视频
     */
    public void imgView(ThumbnailReq req, HttpServletResponse response) throws BBYException {
        DirectLinkObj directLinkObj = getDirectLink(req.getFileId(), req.getDirectId());
        String suffix = FileUtil.getSuffix(directLinkObj.getFilename());
        File file = BBYFileSystem.get(directLinkObj.getMd5());
        logger.debug(file.getPath()+"."+suffix);
        try {
            BufferedImage image;
            if (VideoImgUtil.isVideo(suffix)){
                File imgFile = TempFileManager.createVideoImgFile(directLinkObj.fileId);
                if (!imgFile.exists()) {
                    VideoImgUtil.createVideoImg(file,imgFile,0);
                }
                image = ImageIO.read(imgFile);
            }else {
                image = ImageIO.read(file);
            }
            if (req.getWidth() == null || req.getHeight() == null) {
                Thumbnails.of(image)
                        .size(image.getWidth(), image.getHeight())
                        .outputFormat("jpg")
                        .toOutputStream(response.getOutputStream());
            } else {
                Thumbnails.of(image)
                        .size(req.getWidth(), req.getHeight())
                        .outputFormat("jpg")
                        .toOutputStream(response.getOutputStream());
            }
        } catch (IOException e) {
        }
    }

    @Deprecated
    public void viewVideoImg(String fileId,String directId, HttpServletResponse response) throws BBYException {
        DirectLinkObj directLinkObj = getTempFileDirectLink(fileId, directId);
        File file = new File(directLinkObj.getTempFilePath());
        if (!file.exists()){
            deleteDirectLink(directLinkObj);
            throw new BBYException(StatusCode.FILE_ERROR,"文件已过期！");
        }
        try {
            response.setContentType("image/*");
            FileUtil.writeToStream(file,response.getOutputStream());
        } catch (IOException e) {}
    }
    @Deprecated
    public DirectLinkObj createVideoImg(VirtualFile virtualFile) throws BBYException {
        File file = TempFileManager.createVideoImgFile(virtualFile.getMd5());
        if (!file.exists()) {
            VideoImgUtil.createVideoImg(BBYFileSystem.get(virtualFile.getMd5()), file, 6);
        }
        return createTempDirectLink(virtualFile.getId(),virtualFile.getName(),file.getAbsolutePath());
    }

    public void video(String fileId, String directId, String range, HttpServletResponse response) throws BBYException {
        DirectLinkObj directLinkObj = getDirectLink(fileId, directId);
        File file = directLinkObj.getTempFilePath() == null ?
                BBYFileSystem.get(directLinkObj.getMd5())
                : new File(directLinkObj.getTempFilePath());
        if (file.exists()) {
            long length = file.length();
            //支持断点续传
            try (RandomAccessFile input = new RandomAccessFile(file, "r");
                 ServletOutputStream output = response.getOutputStream()) {
                response.reset();
                response.setHeader("Accept-Ranges", "bytes");
                response.setContentType("video/mp4");
                outputRange(response, range, input, output, length);
            } catch (IOException ie) {
                logger.debug("结束下载");
            }
        }
    }
    /**
     * 开始下载
     */
    public void download(String fileId, String directId, String range, HttpServletResponse response) throws BBYException {
        DirectLinkObj directLinkObj = getDirectLink(fileId, directId);
        File file = directLinkObj.getTempFilePath() == null ?
                BBYFileSystem.get(directLinkObj.getMd5())
                : new File(directLinkObj.getTempFilePath());
        file.setLastModified(directLinkObj.getLastModified());

        long length = file.length();

        // 如果浏览器支持内容类型，则设置为“内联”，否则将弹出“另存为”对话框. attachment inline
        String disposition = "attachment";
        // 将需要下载的文件段发送到客服端，准备流.
        try (RandomAccessFile input = new RandomAccessFile(file, "r");
             ServletOutputStream output = response.getOutputStream()) {
            //最后修改时间
            FileTime lastModifiedObj = Files.getLastModifiedTime(file.toPath());
            long lastModified = LocalDateTime.ofInstant(lastModifiedObj.toInstant(),
                    ZoneId.of(ZoneId.systemDefault().getId())).toEpochSecond(ZoneOffset.UTC);
            //初始化response.
            response.reset();
            response.setBufferSize(20480);
//            response.setHeader("Content-type", "application/octet-stream;charset=UTF-8");
            response.setContentType("video/mp4");
            response.setHeader("Content-Disposition", disposition + ";filename=" +
                    URLEncoder.encode(directLinkObj.getFilename(), StandardCharsets.UTF_8.name()));
            response.setHeader("Accept-Ranges", "bytes");
            response.setHeader("ETag", URLEncoder.encode(directLinkObj.getFilename(), StandardCharsets.UTF_8.name()));
            response.setDateHeader("Last-Modified", lastModified);
            response.setDateHeader("Expires", System.currentTimeMillis() + 604800000L);
            //输出Range到response
            outputRange(response, range, input, output, length);
            output.flush();
            response.flushBuffer();
        } catch (Exception e) {
            logger.debug("文件停止下载");
        }
    }

    /**
     * 处理请求中的Range(多个range或者一个range，每个range范围)
     */
    private void dealRanges(Range full, String range, List<Range> ranges, HttpServletResponse response,
                            long length) throws IOException {
        if (range != null) {
            // Range 头的格式必须为 "bytes=n-n,n-n,n-n...". 如果不是此格式, 返回 416.
            if (!range.matches("^bytes=\\d*-\\d*(,\\d*-\\d*)*$")) {
                response.setHeader("Content-Range", "bytes */" + length);
                response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
                return;
            }

            // 处理传入的range的每一段.
            for (String part : range.substring(6).split(",")) {
                part = part.split("/")[0];
                // 对于长度为100的文件，以下示例返回:
                // 50-80 (50 to 80), 40- (40 to length=100), -20 (length-20=80 to length=100).
                int delimiterIndex = part.indexOf("-");
                long start = Range.sublong(part, 0, delimiterIndex);
                long end = Range.sublong(part, delimiterIndex + 1, part.length());

                //如果未设置起始点，则计算的是最后的 end 个字节；设置起始点为 length-end，结束点为length-1
                //如果未设置结束点，或者结束点设置的比总长度大，则设置结束点为length-1
                if (start == -1) {
                    start = length - end;
                    end = length - 1;
                } else if (end == -1 || end > length - 1) {
                    end = length - 1;
                }

                // 检查Range范围是否有效。如果无效，则返回416.
                if (start > end) {
                    response.setHeader("Content-Range", "bytes */" + length);
                    response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
                    return;
                }
                // 添加Range范围.
                ranges.add(new Range(start, end, length));
            }
        } else {
            //如果未传入Range，默认下载整个文件
            ranges.add(full);
        }
    }

    /**
     * output写流输出到response
     */
    private void outputRange(HttpServletResponse response, String range, RandomAccessFile input,
                             ServletOutputStream output, long length) throws IOException {
        Range full = new Range(0, length - 1, length);
        List<Range> ranges = new ArrayList<>();
        dealRanges(full, range, ranges, response, length);
        if (ranges.isEmpty() || ranges.get(0) == full) {
            // 返回整个文件.
//            response.setContentType("application/octet-stream;charset=UTF-8");
            response.setStatus(HttpServletResponse.SC_OK); // 200.
            response.setHeader("Content-Range", "bytes " + full.start + "-" + full.end + "/" + full.total);
            response.setHeader("Content-Length", String.valueOf(full.length));
            Range.copy(input, output, length, full.start, full.length);
        } else if (ranges.size() == 1) {
            // 返回文件的一个分段.
            Range r = ranges.get(0);
//            response.setContentType("application/octet-stream;charset=UTF-8");
            response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206.
            response.setHeader("Content-Range", "bytes " + r.start + "-" + r.end + "/" + r.total);
            response.setHeader("Content-Length", String.valueOf(r.length));
            // 复制单个文件分段.
            Range.copy(input, output, length, r.start, r.length);
        } else {
            // 返回文件的多个分段.
            response.setContentType("multipart/byteranges; boundary=MULTIPART_BYTERANGES");
            response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206.

            // 复制多个文件分段.
            for (Range r : ranges) {
                //为每个Range添加MULTIPART边界和标题字段
                output.println();
                output.println("--MULTIPART_BYTERANGES");
                output.println("Content-Type: application/octet-stream;charset=UTF-8");
                output.println("Content-length: " + r.length);
                output.println("Content-Range: bytes " + r.start + "-" + r.end + "/" + r.total);
                // 复制多个需要复制的文件分段当中的一个分段.
                Range.copy(input, output, length, r.start, r.length);
            }

            // 以MULTIPART文件的边界结束.
            output.println();
            output.println("--MULTIPART_BYTERANGES--");
        }
    }

}
